Skip to content

fix(dev): stage immutable per-environment output checkpoints before assembling runtime cohorts (#74) - #85

Merged
ScriptedAlchemy merged 5 commits into
mainfrom
cursor/immutable-env-checkpoint-staging-c238
Aug 31, 2026
Merged

fix(dev): stage immutable per-environment output checkpoints before assembling runtime cohorts (#74)#85
ScriptedAlchemy merged 5 commits into
mainfrom
cursor/immutable-env-checkpoint-staging-c238

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Fixes #74. Follow-up to the Rsbuild 2.2.1 conformance audit #72; builds on (does not redo) the bounded hardening merged in #76. Rebased onto main at #80 (560124af).

Problem

The runtime observer correlated MultiStats.children[] by environment name and then copied the mutable writeToDisk roots after global completion. #76 hardened that path (explicit compiler names, exact-child assertions, serialized capture), but generation capture still read live compiler roots as a cross-environment cohort: a parallel compile could rewrite one environment's root between global completion and the copy, letting Stats hashes and copied bytes describe different child moments.

What this does

  • environment-checkpoint-store.ts (new): stages an immutable copy of each environment's completed output root inside Rsbuild's documented onAfterEnvironmentCompile hook, keyed by that compilation's Stats hash. Rsbuild 2.2.1 awaits this hook inside the environment compiler's Rspack done tap (verified against the shipped implementation), so the copy reads a quiescent root before that compiler's next write cycle; writeToDisk flushes through assetEmitted, which completes before done.
  • Cohort assembly by exact hash: the global after-compile observer now collects one hash per environment (rsc, widget, app) and capture acquires a complete checkpoint set matching those exact hashes. Rsbuild dispatches the global hook from the last-completing child without waiting for the other children's async environment hooks, so acquisition waits for a late-staging checkpoint and fails fast once a newer compilation supersedes an awaited hash. sourceRevision stays derived from rsc + widget only, so no-op/changed-cohort classification is unchanged; the App child hash now selects which staged App checkpoint joins the cohort instead of "whatever is on disk at copy time".
  • Digest-verified admission: every byte copied into a generation candidate is verified against the digest recorded when its checkpoint was staged, so a generation's Stats hashes and copied bytes come from the same immutable per-environment checkpoints. The definition executable now runs against (and its generated artifacts land in) the candidate's own copy — the live compiler roots are never written to or read by capture anymore.
  • GC: superseded checkpoints are deleted once unpinned; a pinned cohort stays intact while newer compiles land, and release-time GC never touches an admitted candidate's copied bytes (candidates own their copies). Store close rejects waiters and drains staged directories.
  • Staging-chain validation: fix(dev): harden runtime relay against Rsbuild internals #76's foreign-file defense moved from capture time to staging time — each staged rsc tree must contain only files declared by its own runtime-assets.json or bytes already validated by the previous checkpoint of the same environment. Staging failures are recorded per (environment, hash) and surface loudly at cohort acquisition instead of rejecting inside the compiler's done hook, which would skip Rsbuild's global dispatch and strand the FIFO attempt pairing.

How this differs from #76's serialized copy

#76 kept the live-root copy and made it safer: it serialized same-root captures behind a tail promise and chained an accepted-digest map so stale files could be told apart from foreign writes, while documenting the cross-root timing assumption as empirical. This PR removes that assumption instead of bounding it: capture no longer reads live roots at all, so no serialization is needed — the checkpoint for each environment is taken at the only moment Rsbuild actually guarantees that environment's output is complete and unwritten. #76's explicit Rspack compiler names, exact-child assertions, FIFO attempt pairing, failed/no-op attempt handling, and commit-window protections are all retained (the FIFO block in rsbuild.config.ts is untouched except for the adjacent onAfterEnvironmentCompile registration).

CI flake fix (Examples check)

The Examples check failed on state-and-definition.test.ts > excludes a live heartbeat owner and recovers its stale lock only after SIGKILL: the stale-lock recovery kernel inherited the test-support's fixed 100 ms release/owner-settlement budgets, so one slow lock-directory fs operation on the contended 2-core runner poisoned an otherwise-successful recovered mutation (RuntimeStateLockError: Runtime state lease release exceeded 100 ms…). The recovery assertion is about staleness exclusion, not those budgets (dedicated tests pin them explicitly), so the recovery kernel now uses the scaled production budgets. Unrelated to the checkpoint staging change.

Stay in lane

No changes to #73 (reload channel / proxy frame parsing) or #75 (attempt IDs are assigned and consumed exactly as before). packages/agent-bundle/src/build/rslib.ts, build.test.ts, hooks.test.ts untouched; the rebase over #80 was conflict-free.

Changeset

None: the change lives entirely in examples/rsc-agent-runtime (@agent-bundle/rsc-agent-runtime-demo is private and excluded from versioning by privatePackages.version: false); no published package's runtime behavior changes. #76 needed one only because it also touched packages/agent-bundle.

Test plan

  • pnpm typecheck, pnpm lint (re-run after rebase onto refactor(build): serve generated entries through VirtualModulesPlugin at dedicated virtual paths #80)
  • new environment-checkpoint-store.test.ts: skewed child completion (acquisition waits for a late checkpoint), supersession during assembly wait + already-superseded rejection, staging-failure rejection, pinned-cohort immutability under invalidation with GC on release that leaves the admitted generation intact, unchanged-hash dedupe, close semantics
  • generation-materializer.test.ts (27 tests): stale-chunk tolerance chain, foreign undeclared-file rejection at staging, fresh-store predecessor isolation, observer environment-hash propagation and staging forwarding, enqueue-failure recapture from immutable checkpoints
  • dev-provider.integration.test.ts (37 tests, real Rsbuild sessions, re-run after rebase), including the flake: RSC runtime generation activation can wedge or go permanently stale after a repair rebuild (Node 24 CI, 2x on 2026-08-29) #38 commit-window races now driven through real staged synthetic checkpoints
  • state-and-definition.test.ts (30 tests) with the recovery-budget fix
  • full runtime example suite: 180 passed / 6 skipped
  • pnpm test:unit: 1,775 passed
  • runtime-playground-capture.test.ts with integration config + prebuilt flags (includes compile-error recovery evidence)
  • runtime-playground.e2e.test.ts and runtime-playground-hmr.e2e.test.ts with integration config + prebuilt flags
Open in Web Open in Cursor 

@changeset-bot

changeset-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 38136c4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Aug 31, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@85
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@85
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/rsc-runtime@85

commit: 107e3e4

@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review August 31, 2026 17:34

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 107e3e4be2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread examples/rsc-agent-runtime/src/dev/environment-checkpoint-store.ts Outdated
Comment thread examples/rsc-agent-runtime/src/dev/environment-checkpoint-store.ts
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T17:42:20.742792Z 107e3e4 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

cursoragent and others added 3 commits August 31, 2026 19:44
…put checkpoints before assembling runtime cohorts

Replaces the live-root copy in generation capture with hash-matched
assembly from immutable checkpoints staged in onAfterEnvironmentCompile,
which Rsbuild awaits inside each environment compiler's done tap.

Refs #74

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
…store

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
…e-lock recovery

The SIGKILL recovery kernel inherited the test-support 100ms release and
owner-settlement defaults, so one slow lock-directory fs operation on a
contended CI runner poisoned an otherwise-successful recovered mutation.
The recovery assertion is about staleness exclusion, not those budgets;
dedicated tests pin them explicitly.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
…s and drain in-flight staging on close

A failed staged-directory removal was swallowed after the record left the
live set, so GC never retried, close() reported a clean drain, and the
directory leaked for the session's lifetime. Failed roots are now
remembered, retried once while the store drains, and still-failing roots
reject close so session teardown retains the leak as a structured
environment-checkpoints cleanup failure.

close() also resolved while an in-flight stage() was still copying (or
cleaning up) its staging directory, letting session teardown race that
filesystem work; the drain now waits for every environment's staging tail
to settle before reporting the store drained.
@ScriptedAlchemy
ScriptedAlchemy merged commit 76de3ad into main Aug 31, 2026
8 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the cursor/immutable-env-checkpoint-staging-c238 branch August 31, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stage immutable per-environment outputs before assembling runtime cohorts

2 participants